define(['app', 'clipboard', 'json5', 'ace-language-tools'], function(app, Clipboard, JSON5) {
    app.controller('PojoEvaluatorController', function($scope, $timeout, $http, $routeParams, xUtil, xDialog, toastr) {
        var self = this;
        self.code = '';
        self.parsable = true;
        self.hasText = false;
        self.config = {
            'package': '',
            'class': '',
            'builder': false,
            'primitive': false,
            'gettersAndSetters': true,
            'constructor': false,
            'hashCodeAndEquals': false,
            'toString': false,
            'serializable': false,
            'textType': 'xml'
        };

        var textEditor = null,
            javaViewer = null;

        self.init = function() {
            new Clipboard('#copy', {
                text: function() {
                    return self.code;
                }
            }).on('success', function(e) {
                toastr.success('已复制到剪贴板！');
            });

            $timeout(function() {
                // text editor
                textEditor = ace.edit('text-editor');
                textEditor.setOptions({
                    mode: 'ace/mode/hjson',
                    fontSize: 13,
                    showPrintMargin: false,
                    autoScrollEditorIntoView: true,
                    enableBasicAutocompletion: true,
                    enableSnippets: true,
                    enableLiveAutocompletion: true
                });
                textEditor.on('change', function(e) {
                    var hasText = !/^\s*$/.test(textEditor.getValue());
                    if (self.hasText != hasText) {
                        self.hasText = hasText;
                        $timeout(function() { $scope.$apply(); });
                    }
                });
                textEditor.getSession().on('changeAnnotation', function(e) {
                    // check errors
                    $timeout(function() {
                        var annotations = textEditor.getSession().getAnnotations();
                        var parsable = true;
                        if (!_.isEmpty(annotations)) {
                            if (_.find(annotations, function(annotation) {
                                return annotation.type == 'error';
                            })) {
                                parsable = false;
                            }
                        }
                        if (self.parsable != parsable) {
                            self.parsable = parsable;
                        }
                    });
                });

                if ($routeParams.type && $routeParams.path) {
                    loadMockDocument();
                } else {
                    loadSample();
                }

                $scope.$watch('vm.config.textType', function(newValue) {
                    var modeId = 'ace/mode/' + (newValue == 'json' ? 'hjson' : newValue);
                    if (modeId != textEditor.getOption('mode')) {
                        textEditor.getSession().setMode(modeId);
                    }
                });

                // java viewer
                javaViewer = ace.edit('java-viewer');
                javaViewer.setOptions({
                    mode: 'ace/mode/java',
                    fontSize: 13,
                    readOnly: true,
                    useWorker: false,
                    showPrintMargin: false,
                    highlightActiveLine: false,
                    autoScrollEditorIntoView: true
                });

                $scope.$watch('vm.code', function(newValue) {
                    javaViewer.setValue(newValue, -1);
                    javaViewer.resize();
                });
            });
        }

        function loadSample() {
            self.config['package'] = 'app.auth.bean';
            self.config['class'] = 'User';
            $timeout(function() {
                textEditor.setValue([
                    '<xml>',
                    '    <username>admin</username>',
                    '    <password>123456{type:String}</password>',
                    '</xml>'].join('\n'), -1);
                textEditor.resize();
            });
        }

        function loadMockDocument() {
            $http({
                method: 'POST',
                url: '/' + $routeParams.path + '?origin=true',
                transformResponse: function(response) {
                    return response;
                }
            }).then(function(response) {
                var text = null,
                    suffix = '';
                if ($routeParams.type == 'request') {
                    text = _.compose(function(match) {
                        return match ? match : xUtil.reg.subMatch(response.data, /入参：([\s\S]*)出参：/gm);
                    }, function() {
                        return xUtil.reg.subMatch(response.data, /入参：([\s\S]*)错误：/gm);
                    })().trim();
                    suffix = 'Request';
                } else {
                    suffix = 'Response';
                }

                var extname = $routeParams.path.substring($routeParams.path.lastIndexOf('.')),
                    filename = $routeParams.path.substring($routeParams.path.lastIndexOf('/') + 1),
                    rtextResponse = null;
                filename = filename.substring(0, filename.length - extname.length);

                self.config['class'] = getClassName(filename) + suffix;
                self.config['serializable'] = true;
                if (extname == '.json') {
                    rtextResponse = /^\s*\/\*\*((?!\/\*)[^])+\*\//m;
                    self.config['textType'] = 'json';
                } else if (extname == '.xml') {
                    rtextResponse = /^\s*\<\!--((?!\<\!--)[^])+--\>/m;
                    self.config['textType'] = 'xml';
                }

                if ($routeParams.type == 'response') {
                    text = response.data.replace(rtextResponse, '').trim();
                }

                $timeout(function() {
                    textEditor.setValue(text, -1);
                    textEditor.resize();
                });
            });
        }

        self.evaluate = function(valid) {
            if (valid && self.parsable && self.hasText) {
                var fields = [], lines = [];
                if (self.config['textType'] == 'json') {
                    // JSON => fields
                    var json = JSON5.parse(textEditor.getValue());
                    if (typeof(json) == 'object' && !(json instanceof Array)) {
                        evaluateJSON(json, fields);
                    } else {
                        xDialog.alert('提示信息', 'JSON文本不是Object类型！');
                    }
                } else if (self.config['textType'] == 'xml') {
                    // XML => fields
                    try {
                        var xml = jQuery.parseXML(textEditor.getValue());
                        var xmlDoc = jQuery(xml).find('xml');
                        if (xmlDoc.length == 0) {
                            xDialog.alert('提示信息', 'XML文本没有以【xml】为根结点！');
                        } else {
                            evaluateXML(xmlDoc[0], fields);
                        }
                    } catch (e) {
                        var message = e + '';
                        message = (message.length <= 200 ? message : message.substring(0, 200) + '\n...');
                        xDialog.alert('提示信息', 'XML文本解析错误：<pre>' + message + '</pre>');
                    }
                } else if (self.config['textType'] == 'sql') {
                    // SQL => fields
                    var sql = textEditor.getValue().trim();
                    evaluateSQL(sql, fields);
                }

                // evaluate
                if (self.config['package']) {
                    lines.push('package ' + self.config['package'] + ';');
                    lines.push('');
                }
                if (self.config['serializable']) {
                    lines.push('import java.io.Serializable;');
                    lines.push('');
                }
                lines.push('public class ' + self.config['class']
                        + (self.config['serializable'] ? ' implements Serializable' : '') + ' {');
                lines.push('');
                evaluate(self.config['class'], fields, lines, 1);
                lines.push('}');
                self.code = lines.join('\n');
            }
        }

        function evaluate(clazz, fields, lines, indent) {
            // fields
            _.forEach(fields, function(field) {
                // field
                var type = (field.primitive && self.config['primitive'] ? field.types[1] : field.types[0]);
                lines.push(getIndents(indent) + 'private ' + type + ' ' + field.name + ';');
                lines.push('');

                // sub fields
                if (!field.primitive && !_.isEmpty(field.fields)) {
                    // start of class
                    lines.push(getIndents(indent) + 'public static class ' + field.types[1]
                            + (self.config['serializable'] ? ' implements Serializable' : '') + ' {');
                    lines.push('');
                    // class' fields
                    evaluate(field.types[1], field.fields, lines, indent + 1);
                    // end of class
                    lines.push(getIndents(indent) + '}');
                    lines.push('');
                }
            });

            // constructor
            if (self.config['constructor']) {
                lines.push(getIndents(indent) + 'public ' + clazz + '(' + _.map(fields, function(field) {
                    var type = (field.primitive && self.config['primitive'] ? field.types[1] : field.types[0]);
                    return type + ' ' + field.name;
                }).join(', ') + ') {');
                _.forEach(fields, function(field) {
                    lines.push(getIndents(indent + 1) + 'this.' + field.name + ' = ' + field.name + ';');
                });
                lines.push(getIndents(indent) + '}');
                lines.push('');
            }

            // methods
            if (self.config['gettersAndSetters']) {
                _.forEach(fields, function(field) {
                    var type = (field.primitive && self.config['primitive'] ? field.types[1] : field.types[0]);
                    // getter
                    lines.push(getIndents(indent) + 'public ' + type + ' get' + firstUpperCase(field.name) + '() {');
                    lines.push(getIndents(indent + 1) + 'return ' + field.name + ';');
                    lines.push(getIndents(indent) + '}');
                    lines.push('');
                    // setter
                    var returnType = 'void';
                    if (self.config['builder']) {
                        returnType = clazz;
                    }
                    lines.push(getIndents(indent) + 'public ' + returnType + ' set' + firstUpperCase(field.name)
                            + '(' + type + ' ' + field.name + ') {');
                    lines.push(getIndents(indent + 1) + 'this.' + field.name + ' = ' + field.name + ';');
                    if (self.config['builder']) {
                        lines.push(getIndents(indent + 1) + 'return this;');
                    }
                    lines.push(getIndents(indent) + '}');
                    lines.push('');
                });
            }

            // hashCode & equals
            if (self.config['hashCodeAndEquals']) {
                // hashCode
                lines.push(getIndents(indent) + '@Override');
                lines.push(getIndents(indent) + 'public int hashCode() {');
                lines.push(getIndents(indent + 1) + 'int result = 1;');
                _.forEach(fields, function(field) {
                    if (isPrimitiveType(field.types[1])) {
                        lines.push(getIndents(indent + 1) + 'result = 31 * result + ' + field.name + ';');
                    } else {
                        lines.push(getIndents(indent + 1) + 'result = 31 * result + (' + field.name + ' == null ? 0 : '
                                + field.name + '.hashCode());');
                    }
                });
                lines.push(getIndents(indent + 1) + 'return result;');
                lines.push(getIndents(indent) + '}');
                lines.push('');
                // equals
                lines.push(getIndents(indent) + '@Override');
                lines.push(getIndents(indent) + 'public boolean equals(Object obj) {');
                lines.push(getIndents(indent + 1) + 'if (this == obj) return true;');
                lines.push(getIndents(indent + 1) + 'if (obj == null || getClass() != obj.getClass()) return false;');
                lines.push(getIndents(indent + 1) + clazz + ' other = (' + clazz + ') obj;');
                _.forEach(fields, function(field) {
                    if (isPrimitiveType(field.types[1])) {
                        lines.push(getIndents(indent + 1) + 'if (' + field.name + ' != other.' + field.name
                                + ') return false;');
                    } else {
                        lines.push(getIndents(indent + 1) + 'if (' + field.name + ' != null ? !' + field.name
                                + '.equals(other.' + field.name + ') : other.' + field.name + ' != null) return false;');
                    }
                });
                lines.push(getIndents(indent + 1) + 'return true;');
                lines.push(getIndents(indent) + '}');
                lines.push('');
            }

            // toString
            if (self.config['toString']) {
                lines.push(getIndents(indent) + '@Override');
                lines.push(getIndents(indent) + 'public String toString() {');
                lines.push(getIndents(indent + 1) + 'StringBuilder builder = new StringBuilder();');
                lines.push(getIndents(indent + 1) + 'builder.append("' + clazz + ' [");');
                _.forEach(fields, function(field, i) {
                    if (i == 0) {
                        lines.push(getIndents(indent + 1) + 'builder.append("' + field.name + '=").append('
                                + field.name + ');');
                    } else {
                        lines.push(getIndents(indent + 1) + 'builder.append(", ' + field.name + '=").append('
                                + field.name + ');');
                    }
                });
                lines.push(getIndents(indent + 1) + 'builder.append("]");');
                lines.push(getIndents(indent + 1) + 'return builder.toString();');
                lines.push(getIndents(indent) + '}');
                lines.push('');
            }
        }

        function evaluateJSON(object, fields) {
            _.forEach(object, function(value, name) {
                name = getFieldName(name);
                switch(typeof(value)) {
                    case 'object':
                        var result = { times: 0, type: '' };
                        parseObjectType(name, value, result);
                        if (result['times'] == 0 && result['value']) {
                            var subFields = [];
                            evaluateJSON(result['value'], subFields);
                            fields.push({
                                name: name,
                                types: [result['type'], result['type']],
                                primitive: false,
                                fields: subFields
                            });
                        } else {
                            var type = result['type'];
                            for (var i = result['times']; i > 0; i--) {
                                type = 'List<' + type + '>';
                            }
                            var subFields = [];
                            evaluateJSON(result['value'], subFields);
                            fields.push({
                                name: name,
                                types: [type, result['type']],
                                primitive: false,
                                fields: subFields
                            });
                        }
                        break;
                    case 'boolean':
                    case 'number':
                    case 'string':
                    default:
                        fields.push({
                            name: name,
                            types: parsePrimitiveType(value, true),
                            primitive: true
                        });
                        break;
                }
            });
        }

        function evaluateXML(xmlDoc, fields) {
            _.forEach(jQuery(xmlDoc).children(), function(node) {
                var name = getFieldName(node.tagName),
                    children = jQuery(node).children();
                if (children.length == 0) {
                    fields.push({
                        name: name,
                        types: parseStringType(node.textContent, true),
                        primitive: true
                    });
                } else {
                    var diffSubNode = null;
                    if (children.length > 1) {
                        var tagName = null;
                        diffSubNode = _.find(children, function(subNode) {
                            if (tagName == null) {
                                tagName = subNode.tagName;
                            }
                            return subNode.tagName != tagName;
                        });
                    }
                    if (/*children.length == 1 || */diffSubNode) {
                        var subFields = [], type = firstUpperCase(name);
                        evaluateXML(node, subFields);
                        fields.push({
                            name: name,
                            types: [type, type],
                            primitive: false,
                            fields: subFields
                        });
                    } else if (children.length > 0) {
                        var subNode = children[0];
                        var subFields = [];
                        evaluateXML(subNode, subFields);
                        var type = (jQuery(subNode).children().length == 0 ?
                                parseStringType(subNode.textContent, false) : firstUpperCase(subNode.tagName));
                        fields.push({
                            name: name,
                            types: ['List<' + type + '>', type],
                            primitive: false,
                            fields: subFields
                        });
                    }
                }
            })
        }

        function evaluateSQL(sql, fields) {
            var rfields = /create table[^\(]*\(([\s\S]+)\)/mgi,
                rfield = /\s*([0-9A-Za-z_`]+)\s+([0-9A-Za-z_]+(\(\d+(,\s*\d+)?\))?).*,?/g,
                rtypes = /([0-9A-Za-z_]+)(\((\d+)(,\s*(\d+))?\))?/,
                match = null;

            match = rfields.exec(sql);
            if (match && match.length > 1) {
                var fieldsString = match[1];
                // fields
                while ((match = rfield.exec(fieldsString))) {
                    // remove key fields
                    if (!/^\s+(primary|unique|constraint|foreign)\s+key/i.test(match[0])) {
                        var name = getFieldName(match[1].replace(/[^0-9A-Za-z_]/g, '')),
                            types = matchPrimitiveType(match[0]);
                        if (types == null) {
                            // match field type
                            var fieldType = {},
                                typeMatch = rtypes.exec(match[2]);
                            if (typeMatch) {
                                fieldType['type'] = typeMatch[1].toUpperCase();
                                if (typeMatch[3]) {
                                    fieldType['precision'] = parseInt(typeMatch[3]);
                                }
                                if (typeMatch[5]) {
                                    fieldType['scale'] = parseInt(typeMatch[5]);
                                }
                            }
                            // field type mapping
                            switch(fieldType['type']) {
                                case 'CHAR':
                                case 'VARCHAR':
                                case 'TEXT':
                                    types = ['String', 'String'];
                                    break;
                                case 'NUMBER':
                                    if (fieldType['scale'] && fieldType['scale'] > 0) {
                                        types = ['Double', 'double'];
                                        break;
                                    }
                                case 'TINYINT':
                                case 'SMALLINT':
                                case 'MEDIUMINT':
                                case 'INT':
                                case 'INTEGER':
                                    types = ['Integer', 'int'];
                                    break;
                                case 'BIGINT':
                                    types = ['Long', 'long'];
                                    break;
                                case 'FLOAT':
                                    types = ['Float', 'float'];
                                    break;
                                case 'DOUBLE':
                                case 'DECIMAL':
                                case 'NUMERIC':
                                    types = ['Double', 'double'];
                                    break;
                                case 'DATE':
                                case 'TIME':
                                case 'DATETIME':
                                case 'TIMESTAMP':
                                    types = ['Date', 'Date'];
                                    break;
                                default:
                                    types = ['Object', 'Object'];
                                    break;
                            }
                        }
                        fields.push({
                            name: name,
                            types: types,
                            primitive: true
                        });
                    }
                }
            }
        }

        function parseStringType(value, primitive) {
            var types = null;
            if (value == '') {
                types = ['Object', 'Object'];
            } else {
                types = matchPrimitiveType(value);
                if (types == null) {
                    if (/^(true|false)$/i.test(value)) {
                        types =  ['Boolean', 'boolean'];
                    } else if (/^-?\d+$/.test(value)) {
                        var number = eval(value);
                        if ((number + 0x80000000) >= 0 && (number - 0x7fffffff) <= 0) {
                            types = ['Integer', 'int'];
                        } else {
                            types = ['Long', 'long'];
                        }
                    } else if (/^(-?\d+)(\.\d+)?$/.test(value)) {
                        types = ['Double', 'double'];
                    } else {
                        types = ['String', 'String'];
                    }
                }
            }
            return (primitive ? types : types[0]);
        }

        function parseObjectType(name, value, result) {
            if (value == null) {
                result['type'] = 'Object';
            } else if (typeof(value) == 'object') {
                if (value instanceof Array) {
                    result['times']++;
                    if (value.length > 0) {
                        parseObjectType(name, value[0], result);
                    } else {
                        result['type'] = 'Object';
                    }
                } else {
                    result['value'] = value;
                    result['type'] = firstUpperCase(name);
                }
            } else {
                result['type'] = parsePrimitiveType(value, false);
            }
        }

        function parsePrimitiveType(value, primitive) {
            var types = matchPrimitiveType(value);
            if (types == null) {
                switch(typeof(value)) {
                    case 'string':
                        types = ['String', 'String'];
                        break;
                    case 'boolean':
                        types = ['Boolean', 'boolean'];
                        break;
                    case 'number':
                        if (/^-?\d+$/.test(value + '')) {
                            if ((value + 0x80000000) >= 0 && (value - 0x7fffffff) <= 0) {
                                types = ['Integer', 'int'];
                            } else {
                                types = ['Long', 'long'];
                            }
                        } else {
                            types = ['Double', 'double'];
                        }
                        break;
                    default:
                        types = ['Object', 'Object'];
                        break;
                }
            }
            return (primitive ? types : types[0]);
        }

        function matchPrimitiveType(value) {
            var match = (value + '').match(/\{type:([0-9A-Za-z]+)\}/);
            if (match) {
                switch(match[1]) {
                    case 'Boolean':
                        return ['Boolean', 'boolean'];
                    case 'Character':
                        return ['Character', 'char'];
                    case 'Byte':
                        return ['Byte', 'byte'];
                    case 'Short':
                        return ['Short', 'short'];
                    case 'Integer':
                        return ['Integer', 'int'];
                    case 'Long':
                        return ['Long', 'long'];
                    case 'Float':
                        return ['Float', 'float'];
                    case 'Double':
                        return ['Double', 'double'];
                    default:
                        return [match[1], match[1]];
                }
            }
            return null;
        }

        function isPrimitiveType(type) {
            return /^(boolean|char|byte|short|int|long|float|double)$/.test(type);
        }

        function getIndents(indent) {
            return _.map(_.range(indent), function() {
                return '    ';
            }).join('');
        }

        function getCamelCaseName(name) {
            if (name == name.toUpperCase()) {
                name = name.toLowerCase();
            }
            var fieldName = '',
                nextUpperCase = false;
            for (var i = 0; i < name.length; i++) {
                var char = name.charAt(i);
                switch (char) {
                    case '_':
                    case '-':
                    case '$':
                        if (fieldName.length > 0) {
                            nextUpperCase = true;
                        }
                        break;
                    default:
                        if (nextUpperCase) {
                            fieldName += char.toUpperCase();
                            nextUpperCase = false;
                        } else {
                            fieldName += char;
                        }
                        break;
                }
            }
            return fieldName;
        }

        function getFieldName(name) {
            return firstLowerCase(getCamelCaseName(name));
        }

        function getClassName(name) {
            return firstUpperCase(getCamelCaseName(name));
        }

        function firstUpperCase(name) {
            return name.substring(0, 1).toUpperCase() + name.substring(1);
        }

        function firstLowerCase(name) {
            return name.substring(0, 1).toLowerCase() + name.substring(1);
        }

        self.callback = function(height, width) {
            jQuery('.x-evaltext').css('height', (height - 175) + 'px');
            jQuery('.x-evaltext .x-editor pre').css('min-height', (height - 175) + 'px');
        }

        self.save = function() {
            xUtil.file.save(self.config['class'] + '.java', self.code);
        }

        self.reset = function() {
            self.code = '';
            textEditor.setValue('', -1);
            self.config = {
                'package': '',
                'class': '',
                'builder': false,
                'primitive': false,
                'gettersAndSetters': true,
                'constructor': false,
                'hashCodeAndEquals': false,
                'toString': false,
                'serializable': false,
                'textType': 'xml'
            };
        }
    });
});
